iT邦幫忙

2025 iThome 鐵人賽

DAY 10
0
Rust

大家一起跟Rust當好朋友吧!系列 第 10

Day 10: 泛型 (Generics):寫出彈性又抽象的程式碼

  • 分享至 

  • xImage
  •  

嗨嗨!大家好!歡迎來到 Rust 三十天挑戰的第十天!

經過前九天的學習,我們已經掌握了 Rust 的基礎語法、所有權系統、錯誤處理等核心概念。今天我們要學習一個能讓程式碼變得更加靈活和可重用的重要特性:泛型 (Generics)

如果說前面學的是如何寫出「正確」的 Rust 程式碼,那麼泛型就是教我們如何寫出「優雅」的程式碼。想像一下,如果每次需要處理不同型別的資料時,就要複製貼上一堆相似的程式碼,那會有多麻煩?泛型就是解決這個問題的利器!

老實說,剛開始接觸泛型時,我覺得那些尖括號 <T> 看起來很嚇人,感覺像是在學某種神秘的數學符號。但當我理解了泛型的本質——它只是讓我們寫出「適用於多種型別」的程式碼的方式——一切就變得清晰了。今天讓我們一起揭開泛型的神秘面紗!

什麼是泛型?為什麼需要它?

問題:重複的程式碼

讓我們先看一個沒有泛型的例子,體會一下重複程式碼的痛苦:

// 找出 i32 向量中的最大值
fn largest_i32(list: &[i32]) -> &i32 {
    let mut largest = &list[0];
    
    for item in list {
        if item > largest {
            largest = item;
        }
    }
    
    largest
}

// 找出 char 向量中的最大值
fn largest_char(list: &[char]) -> &char {
    let mut largest = &list[0];
    
    for item in list {
        if item > largest {
            largest = item;
        }
    }
    
    largest
}

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];
    let result = largest_i32(&number_list);
    println!("最大的數字是 {}", result);

    let char_list = vec!['y', 'm', 'a', 'q'];
    let result = largest_char(&char_list);
    println!("最大的字元是 {}", result);
}

你有沒有發現,除了型別不同,這兩個函式的邏輯完全一樣?這就是重複程式碼的典型案例。

解決方案:泛型

使用泛型,我們可以將上面的兩個函式合併成一個:

fn largest<T: PartialOrd>(list: &[T]) -> &T {
    let mut largest = &list[0];
    
    for item in list {
        if item > largest {
            largest = item;
        }
    }
    
    largest
}

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];
    let result = largest(&number_list);
    println!("最大的數字是 {}", result);

    let char_list = vec!['y', 'm', 'a', 'q'];
    let result = largest(&char_list);
    println!("最大的字元是 {}", result);
}

厲害吧!一個函式就解決了所有可比較型別的最大值查找問題。這就是泛型的威力!非常的優雅!

泛型語法基礎

型別參數命名慣例

在 Rust 中,泛型型別參數通常使用大寫字母命名:

// 常見的泛型型別參數名稱
T    // Type(型別)- 最常用的泛型參數
E    // Error(錯誤)- 通常用於 Result<T, E>
K    // Key(鍵)- 通常用於 HashMap<K, V>
V    // Value(值)- 通常用於 HashMap<K, V>
R    // Result(結果)- 函式回傳型別
I    // Iterator(迭代器)

基本語法結構

// 函式中的泛型
fn function_name<T>(parameter: T) -> T {
    // 函式體
}

// 結構中的泛型
struct StructName<T> {
    field: T,
}

// 列舉中的泛型
enum EnumName<T> {
    Variant(T),
}

// 方法中的泛型
impl<T> StructName<T> {
    fn method(&self) -> &T {
        &self.field
    }
}

函式中的泛型

單一型別參數

// 簡單的泛型函式
fn print_value<T: std::fmt::Display>(value: T) {
    println!("值是:{}", value);
}

// 交換兩個值的函式
fn swap<T>(a: &mut T, b: &mut T) {
    std::mem::swap(a, b);
}

fn main() {
    print_value(42);
    print_value("Hello");
    print_value(3.14);
    
    let mut x = 5;
    let mut y = 10;
    println!("交換前:x = {}, y = {}", x, y);
    swap(&mut x, &mut y);
    println!("交換後:x = {}, y = {}", x, y);
}

多個型別參數

// 擁有多個型別參數的函式
fn create_pair<T, U>(first: T, second: U) -> (T, U) {
    (first, second)
}

// 型別轉換函式
fn convert_and_combine<T, U, R>(
    a: T, 
    b: U, 
    converter: fn(T, U) -> R
) -> R {
    converter(a, b)
}

fn main() {
    let pair1 = create_pair(42, "hello");
    let pair2 = create_pair(3.14, true);
    let pair3 = create_pair("world", vec![1, 2, 3]);
    
    println!("配對1:{:?}", pair1);
    println!("配對2:{:?}", pair2);
    println!("配對3:{:?}", pair3);
    
    // 使用轉換函式
    let result = convert_and_combine(
        10, 
        20, 
        |a, b| format!("{} + {} = {}", a, b, a + b)
    );
    println!("轉換結果:{}", result);
}

結構中的泛型

基本結構泛型

// 一個可以儲存任何型別的容器
#[derive(Debug)]
struct Container<T> {
    value: T,
}

impl<T> Container<T> {
    fn new(value: T) -> Self {
        Container { value }
    }
    
    fn get(&self) -> &T {
        &self.value
    }
    
    fn set(&mut self, value: T) {
        self.value = value;
    }
    
    // 轉換成另一種型別的容器
    fn map<U, F>(self, f: F) -> Container<U>
    where
        F: FnOnce(T) -> U,
    {
        Container::new(f(self.value))
    }
}

fn main() {
    let mut int_container = Container::new(42);
    println!("整數容器:{:?}", int_container);
    
    let string_container = Container::new(String::from("Hello"));
    println!("字串容器:{:?}", string_container);
    
    // 修改值
    int_container.set(100);
    println!("修改後:{:?}", int_container);
    
    // 型別轉換
    let string_from_int = int_container.map(|x| format!("數字:{}", x));
    println!("轉換後:{:?}", string_from_int);
}

多型別參數的結構

#[derive(Debug)]
struct Point<T, U> {
    x: T,
    y: U,
}

impl<T, U> Point<T, U> {
    fn new(x: T, y: U) -> Self {
        Point { x, y }
    }
    
    fn x(&self) -> &T {
        &self.x
    }
    
    fn y(&self) -> &U {
        &self.y
    }
}

// 只為特定型別組合實現方法
impl Point<f64, f64> {
    fn distance_from_origin(&self) -> f64 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

// 混合型別的方法
impl<T, U> Point<T, U> {
    fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> {
        Point {
            x: self.x,
            y: other.y,
        }
    }
}

fn main() {
    let int_point = Point::new(5, 10);
    let float_point = Point::new(1.0, 4.0);
    let mixed_point = Point::new(5, 4.0);
    
    println!("整數座標:{:?}", int_point);
    println!("浮點數座標:{:?}", float_point);
    println!("混合座標:{:?}", mixed_point);
    
    // 只有 f64 座標能計算距離
    println!("距離原點:{:.2}", float_point.distance_from_origin());
    
    // 混合不同的點
    let result = int_point.mixup(float_point);
    println!("混合結果:{:?}", result);
}

列舉中的泛型

我們其實已經在前面的學習中遇到過泛型列舉了!

回顧熟悉的泛型列舉

// Option<T> 是標準函式庫中的泛型列舉
enum Option<T> {
    Some(T),
    None,
}

// Result<T, E> 也是泛型列舉
enum Result<T, E> {
    Ok(T),
    Err(E),
}

自訂泛型列舉

#[derive(Debug)]
enum Message<T> {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    Data(T),  // 可以攜帶任何型別的資料
}

impl<T> Message<T> {
    fn process(&self) 
    where 
        T: std::fmt::Debug,
    {
        match self {
            Message::Quit => println!("收到退出訊息"),
            Message::Move { x, y } => println!("移動到 ({}, {})", x, y),
            Message::Write(text) => println!("寫入:{}", text),
            Message::Data(data) => println!("資料:{:?}", data),
        }
    }
}

// 多型別參數的列舉
#[derive(Debug)]
enum Either<L, R> {
    Left(L),
    Right(R),
}

impl<L, R> Either<L, R> {
    fn is_left(&self) -> bool {
        matches!(self, Either::Left(_))
    }
    
    fn is_right(&self) -> bool {
        matches!(self, Either::Right(_))
    }
    
    // 轉換左側值
    fn map_left<T, F>(self, f: F) -> Either<T, R>
    where
        F: FnOnce(L) -> T,
    {
        match self {
            Either::Left(l) => Either::Left(f(l)),
            Either::Right(r) => Either::Right(r),
        }
    }
    
    // 轉換右側值
    fn map_right<T, F>(self, f: F) -> Either<L, T>
    where
        F: FnOnce(R) -> T,
    {
        match self {
            Either::Left(l) => Either::Left(l),
            Either::Right(r) => Either::Right(f(r)),
        }
    }
}

fn main() {
    let messages = vec![
        Message::Quit,
        Message::Move { x: 10, y: 20 },
        Message::Write(String::from("Hello")),
        Message::Data(42),
        Message::Data(vec![1, 2, 3]),
    ];
    
    for msg in messages {
        msg.process();
    }
    
    // Either 的使用
    let left: Either<i32, String> = Either::Left(42);
    let right: Either<i32, String> = Either::Right(String::from("hello"));
    
    println!("left is left: {}", left.is_left());
    println!("right is right: {}", right.is_right());
    
    // 轉換操作
    let doubled = Either::Left(21).map_left(|x| x * 2);
    let uppercase = Either::Right(String::from("world")).map_right(|s| s.to_uppercase());
    
    println!("雙倍:{:?}", doubled);
    println!("大寫:{:?}", uppercase);
}

方法定義中的泛型

impl 區塊中的泛型

#[derive(Debug)]
struct Pair<T> {
    first: T,
    second: T,
}

// 為所有型別 T 實現的方法
impl<T> Pair<T> {
    fn new(first: T, second: T) -> Self {
        Self { first, second }
    }
    
    fn into_tuple(self) -> (T, T) {
        (self.first, self.second)
    }
}

// 只為特定型別實現的方法
impl Pair<i32> {
    fn sum(&self) -> i32 {
        self.first + self.second
    }
}

// 為實現了特定 trait 的型別實現方法
impl<T: std::fmt::Display + PartialOrd> Pair<T> {
    fn cmp_display(&self) {
        if self.first >= self.second {
            println!("最大的成員是 first = {}", self.first);
        } else {
            println!("最大的成員是 second = {}", self.second);
        }
    }
}

fn main() {
    let int_pair = Pair::new(10, 20);
    let string_pair = Pair::new(String::from("hello"), String::from("world"));
    
    println!("整數對:{:?}", int_pair);
    println!("字串對:{:?}", string_pair);
    
    // 只有 i32 配對有 sum 方法
    println!("總和:{}", int_pair.sum());
    
    // 只有實現了 Display + PartialOrd 的型別才有這個方法
    int_pair.cmp_display();
    string_pair.cmp_display();
}

方法中的額外泛型參數

#[derive(Debug)]
struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    // 方法本身也可以有泛型參數
    fn distance_to<U>(&self, other: &Point<U>) -> f64 
    where
        T: Into<f64> + Copy,
        U: Into<f64> + Copy,
    {
        let dx = self.x.into() - other.x.into();
        let dy = self.y.into() - other.y.into();
        (dx * dx + dy * dy).sqrt()
    }
    
    // 型別轉換方法
    fn convert<U, F>(self, converter: F) -> Point<U>
    where
        F: Fn(T) -> U,
    {
        Point {
            x: converter(self.x),
            y: converter(self.y),
        }
    }
}

fn main() {
    let int_point = Point { x: 3, y: 4 };
    let float_point = Point { x: 1.0, y: 2.0 };
    
    println!("距離:{:.2}", int_point.distance_to(&float_point));
    
    // 型別轉換
    let string_point = int_point.convert(|x| format!("值:{}", x));
    println!("轉換後:{:?}", string_point);
}

泛型的約束:Trait Bounds

到目前為止,你可能已經注意到我們經常使用 where 子句和 : 語法。這就是 Trait Bounds,用來約束泛型參數必須實現特定的 trait。

基本 Trait Bounds

use std::fmt::Display;

// 直接在泛型參數上指定約束
fn print_and_return<T: Display>(value: T) -> T {
    println!("值:{}", value);
    value
}

// 多個約束
fn compare_and_print<T: Display + PartialOrd>(a: T, b: T) {
    if a > b {
        println!("{} 大於 {}", a, b);
    } else if a < b {
        println!("{} 小於 {}", a, b);
    } else {
        println!("{} 等於 {}", a, b);
    }
}

// 使用 where 子句(讓泛型所代表的定義更清楚)
fn complex_function<T, U>(a: T, b: U) -> String
where
    T: Display + Clone,
    U: Display + Into<String>,
{
    format!("a: {}, b: {}, a_cloned: {}", a, a.clone(), b.into())
}

fn main() {
    let number = print_and_return(42);
    let text = print_and_return("Hello");
    
    compare_and_print(10, 20);
    compare_and_print("apple", "banana");
    
    let result = complex_function(42, String::from("world"));
    println!("複雜函式結果:{}", result);
}

泛型的效能:零成本抽象

Rust 的泛型使用「單態化」(monomorphization) 技術,這意味著泛型程式碼在編譯時會被展開成具體型別的版本,沒有執行時開銷!

// 這個泛型函式...
fn add<T: std::ops::Add<Output = T>>(a: T, b: T) -> T {
    a + b
}

// 在編譯時會被展開成類似這樣:
// fn add_i32(a: i32, b: i32) -> i32 { a + b }
// fn add_f64(a: f64, b: f64) -> f64 { a + b }
// fn add_string(a: String, b: String) -> String { a + b }

fn main() {
    let int_sum = add(5, 10);           // 呼叫 add_i32
    let float_sum = add(3.14, 2.86);    // 呼叫 add_f64
    let string_sum = add(             // 呼叫 add_string
        String::from("Hello, "), 
        String::from("World!")
    );
    
    println!("整數和:{}", int_sum);
    println!("浮點數和:{}", float_sum);
    println!("字串和:{}", string_sum);
}

這種設計讓 Rust 的泛型既靈活又高效,完全沒有動態分派的開銷!

常見的泛型模式

1. Builder 模式

#[derive(Debug)]
struct Config<T, U> {
    host: T,
    port: U,
    debug: bool,
}

struct ConfigBuilder<T, U> {
    host: Option<T>,
    port: Option<U>,
    debug: bool,
}

impl<T, U> ConfigBuilder<T, U> {
    fn new() -> Self {
        ConfigBuilder {
            host: None,
            port: None,
            debug: false,
        }
    }
    
    fn host(mut self, host: T) -> Self {
        self.host = Some(host);
        self
    }
    
    fn port(mut self, port: U) -> Self {
        self.port = Some(port);
        self
    }
    
    fn debug(mut self, debug: bool) -> Self {
        self.debug = debug;
        self
    }
    
    fn build(self) -> Result<Config<T, U>, String> {
        let host = self.host.ok_or("Host is required")?;
        let port = self.port.ok_or("Port is required")?;
        
        Ok(Config {
            host,
            port,
            debug: self.debug,
        })
    }
}

fn main() {
    let config = ConfigBuilder::new()
        .host("localhost")
        .port(8080)
        .debug(true)
        .build()
        .unwrap();
    
    println!("設定:{:?}", config);
}

2. 工廠模式

trait Factory<T> {
    fn create(&self) -> T;
}

struct NumberFactory;
struct StringFactory;

impl Factory<i32> for NumberFactory {
    fn create(&self) -> i32 {
        42
    }
}

impl Factory<String> for StringFactory {
    fn create(&self) -> String {
        "Hello, Factory!".to_string()
    }
}

fn create_item<T, F: Factory<T>>(factory: &F) -> T {
    factory.create()
}

fn main() {
    let number = create_item(&NumberFactory);
    let text = create_item(&StringFactory);
    
    println!("數字:{}", number);
    println!("文字:{}", text);
}

3. 迭代器模式

struct RangeIterator<T> {
    current: T,
    end: T,
    step: T,
}

impl<T> RangeIterator<T>
where
    T: Copy + PartialOrd + std::ops::Add<Output = T>,
{
    fn new(start: T, end: T, step: T) -> Self {
        RangeIterator {
            current: start,
            end,
            step,
        }
    }
}

impl<T> Iterator for RangeIterator<T>
where
    T: Copy + PartialOrd + std::ops::Add<Output = T>,
{
    type Item = T;
    
    fn next(&mut self) -> Option<Self::Item> {
        if self.current < self.end {
            let current = self.current;
            self.current = self.current + self.step;
            Some(current)
        } else {
            None
        }
    }
}

fn main() {
    // 整數範圍
    let int_range = RangeIterator::new(0, 10, 2);
    for num in int_range {
        print!("{} ", num);
    }
    println!();
    
    // 浮點數範圍
    let float_range = RangeIterator::new(0.0, 5.0, 0.5);
    for num in float_range {
        print!("{:.1} ", num);
    }
    println!();
}

泛型的最佳實務

1. 有意義的型別參數名稱

// ✅ 好的命名
struct Cache<Key, Value> {
    data: std::collections::HashMap<Key, Value>,
}

struct Parser<Input, Output, Error> {
    // ...
}

// ❌ 不好的命名
struct Cache<T, U> {  // T 和 U 沒有表達清楚意圖
    data: std::collections::HashMap<T, U>,
}

2. 適當的約束

// ✅ 好:只在需要時添加約束
fn process_data<T>(data: Vec<T>) -> Vec<T> {
    // 不需要約束的操作
    data
}

fn print_data<T: std::fmt::Display>(data: &[T]) {
    // 需要 Display 約束才能印出
    for item in data {
        println!("{}", item);
    }
}

// ❌ 不好:過度約束
fn store_data<T: Clone + Debug + PartialEq + Ord>(data: T) -> T {
    // 實際上只需要移動資料,不需要這麼多約束
    data
}

3. 使用關聯型別

// 當只有一個合理的型別選擇時,使用關聯型別
trait Iterator {
    type Item;  // 關聯型別
    fn next(&mut self) -> Option<Self::Item>;
}

// 而不是泛型參數
trait BadIterator<T> {  // 這樣會讓同一個型別可以實現多次
    fn next(&mut self) -> Option<T>;
}

泛型與效能的深入討論

編譯時多態 vs 執行時多態

// 編譯時多態(泛型)- 零成本
fn generic_add<T: std::ops::Add<Output = T>>(a: T, b: T) -> T {
    a + b  // 編譯時就知道具體的 + 實現
}

// 執行時多態(trait object)- 有虛擬呼叫成本
fn dynamic_add(a: Box<dyn std::ops::Add<Output = i32>>, b: i32) -> i32 {
    // 執行時透過虛擬函式表查找方法
    unimplemented!() // 這只是示例,實際上 Add trait 不能這樣用
}

// 實際效能比較
use std::time::Instant;

fn performance_test() {
    let iterations = 10_000_000;
    
    // 泛型版本
    let start = Instant::now();
    let mut sum = 0;
    for i in 0..iterations {
        sum = generic_add(sum, i);
    }
    let generic_time = start.elapsed();
    
    println!("泛型耗時: {:?}, 結果: {}", generic_time, sum);
}

今天的收穫

今天我們深入學習了 Rust 的泛型系統:

核心概念

  • 泛型語法<T> 在函式、結構、列舉、方法中的使用
  • Trait Bounds:使用 :where 子句約束泛型參數
  • 單態化:編譯時展開泛型,實現零成本抽象
  • 條件實現:只為滿足特定條件的型別實現方法

實用技巧

  • 多型別參數:處理複雜的型別組合
  • 關聯型別 vs 泛型參數:選擇合適的抽象方式
  • 方法中的泛型:額外的型別參數
  • 鏈式操作:設計流暢的 API

設計模式

  • 容器型別:通用的資料結構
  • Builder 模式:類型安全的物件建構
  • 工廠模式:泛型的物件創建
  • 迭代器模式:通用的遍歷抽象

最佳實務

  • 有意義的型別參數命名
  • 適當的約束層級
  • 效能考量與權衡
  • API 設計的一致性

為什麼泛型很重要?

  • 減少重複:一套程式碼處理多種型別
  • 型別安全:編譯時檢查,執行時無錯
  • 零成本抽象:高階抽象,原生效能
  • 組合性:與其他語言特性完美結合

泛型是現代程式語言的重要特性,Rust 的泛型系統在保持零成本的同時,提供了極高的表達力和安全性。掌握泛型將讓你能夠寫出更加抽象、更可重用、但同樣高效的程式碼。

今天的小挑戰

為了鞏固今天的學習,嘗試實作一個通用的緩存系統

功能需求

  1. 基本緩存:支援 get, put, remove 操作
  2. 容量限制:超過容量時移除最舊的項目 (LRU)
  3. 過期機制:支援設定項目的生存時間
  4. 統計功能:追蹤命中率、大小等資訊
  5. 序列化:支援載入/保存緩存到檔案

泛型要求

  • K 必須可比較和雜湊
  • V 必須可克隆
  • 支援不同的過期策略(泛型)
  • 支援不同的序列化格式(泛型)

技術提示

use std::collections::HashMap;
use std::hash::Hash;
use std::time::{Duration, Instant};

#[derive(Debug, Clone)]
struct CacheEntry<V> {
    value: V,
    created_at: Instant,
    last_accessed: Instant,
    ttl: Option<Duration>,
}

struct Cache<K, V, E = NoExpiration> 
where
    K: Hash + Eq + Clone,
    V: Clone,
    E: ExpirationPolicy<V>,
{
    data: HashMap<K, CacheEntry<V>>,
    capacity: usize,
    expiration_policy: E,
    // 統計資訊
    hits: u64,
    misses: u64,
}

trait ExpirationPolicy<V> {
    fn is_expired(&self, entry: &CacheEntry<V>) -> bool;
    fn on_access(&self, entry: &mut CacheEntry<V>);
}

struct NoExpiration;
struct TimeBasedExpiration {
    default_ttl: Duration,
}

// 你來實現這些功能!

這個挑戰將讓你綜合運用泛型的各種特性:型別參數、trait bounds、條件實現、關聯型別等。重點是設計一個既靈活又易用的泛型 API。

明天我們將學習 Traits(特徵),這是 Rust 實現多型和程式碼共享的核心機制。Traits 與泛型結合使用,能讓我們寫出更加抽象和強大的程式碼!

如果在實作過程中遇到任何問題,歡迎在留言區討論。泛型是一個需要多練習才能熟練的概念,但一旦掌握,它會成為你最強大的工具之一!

我們明天見!


上一篇
Day 9: 錯誤處理 (Error Handling):從 `panic!` 到 `Result`
下一篇
Day 11: 特徵 (Traits):定義共享行為 - Rust 的多型與介面系統
系列文
大家一起跟Rust當好朋友吧!19
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言